/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <string>
#include <time.h>
#include <vector>

#include "runner_utils/log.h"
#include "runner_utils/napi_utils.h"

#include "js_utils/js_timer.h"

#define LOG_TAG "Timer"
#define TIMER_LOG(level) RUNNER_LOG(LOG_TAG, level)

int JsTimer::Init()
{
    uv_loop_t *loop = nullptr;
    if (napi_get_uv_event_loop(env_, &loop) != napi_ok)
    {
        TIMER_LOG(ERROR) << "Failed to get uv_loop, timer init failed.";
        return -1;
    }
    if (uv_check_init(loop, &handle_) != 0)
    {
        TIMER_LOG(ERROR) << "Failed to init uv_check, timer init failed.";
        return -2;
    }
    uv_unref(reinterpret_cast<uv_handle_t *>(&handle_));
    if (uv_check_start(&handle_, UvCheckCallback) != 0)
    {
        TIMER_LOG(ERROR) << "Failed to start uv_check, timer init failed.";
        return -3;
    }
    handle_.data = this;
    napi_add_env_cleanup_hook(env_, CleanUpHook, this);
    return 0;
}

int JsTimer::Deinit()
{
    auto it = taskList_.begin();
    while (it != taskList_.end())
    {
        delete it->second;
        it->second = nullptr;
        taskList_.erase(it++);
    }

    uv_loop_t *loop = nullptr;
    if (napi_get_uv_event_loop(env_, &loop) != napi_ok)
    {
        TIMER_LOG(ERROR) << "Failed to get uv_loop, timer deinit failed.";
        return -1;
    }
    if (uv_check_stop(&handle_) != 0)
    {
        TIMER_LOG(ERROR) << "Failed to stop uv_check, timer deinit failed.";
        return -2;
    }
    uv_close(reinterpret_cast<uv_handle_t *>(&handle_), nullptr);
    handle_.data = nullptr;
    napi_remove_env_cleanup_hook(env_, CleanUpHook, this);
    return 0;
}

void JsTimer::CheckCallback()
{
    bool exceptionPending = false;
    napi_status status = napi_is_exception_pending(env_, &exceptionPending);
    if (status != napi_ok || exceptionPending)
    {
        for (auto [id, task] : taskList_)
        {
            delete task;
        }
        taskList_.clear();
    }
}

uint32_t JsTimer::CreateTimerTask(napi_env env, const utils::NapiCallbackInfo &info, TimerType timerType)
{
    if (timerType == TimerType::DEFAULT)
    {
        return 0;
    };

    constexpr size_t minArgc = 1; // min arg count
    if (utils::TypeOf(env, info[0]) != napi_function)
    {
        return 0;
    }
    TimerTask *task = TimerTask::Create(env, this, timerType);
    napi_create_reference(env, info[0], 1, &task->callback_);
    uint32_t timeout = 0;
    if (info.GetArgc() > 1)
    {
        napi_value timeoutVal = nullptr;
        napi_coerce_to_number(env, info[1], &timeoutVal);
        napi_get_value_uint32(env, timeoutVal, &timeout);
    }

    if (info.GetArgc() > 2)
    {
        for (uint32_t i = 2; i < info.GetArgc(); i++) // 2: start index of args
        {
            task->args_.push_back(utils::CreateReference(env, info[i]));
        }
    }

    if (task->Init(timeout) != 0)
    {
        delete task;
        task = nullptr;
        return 0;
    }

    if (task != nullptr)
    {
        taskList_.emplace(nextTaskId_, task);
    }
    return nextTaskId_++;
};

uint32_t JsTimer::CreateWeakTimerTask(napi_env env, const utils::NapiCallbackInfo &info, TimerType timerType)
{
    const uint32_t id = CreateTimerTask(env, info, timerType);
    if (id == 0)
    {
        return id;
    }
    TimerTask *task = taskList_.at(id);
    if (task != nullptr)
    {
        task->Unref();
    }
    return id;
}

void JsTimer::CancelTimer(uint32_t id)
{
    auto task = taskList_.find(id);
    if (task != taskList_.end())
    {
        delete task->second;
        taskList_.erase(task);
    };
}

void JsTimer::CancelTimer(TimerTask *timer)
{
    for (auto it = taskList_.begin(); it != taskList_.end(); it++)
    {
        if (it->second == timer)
        {
            delete it->second;
            taskList_.erase(it);
            break;
        }
    }
}

#define CHECK_ARGS_AND_RETURN(argc, required, returnVal) \
    if (argc < required)                                 \
    {                                                    \
        return returnVal;                                \
    }

#define CHECK_ARGS_AND_THROW(name, argc, required, returnVal)                                               \
    if (argc < required)                                                                                    \
    {                                                                                                       \
        std::string errMessage =                                                                            \
            "Failed to execute 'set" #name "' on 'globalThis': " #required " argument required, but only "; \
        errMessage += std::to_string(argc);                                                                 \
        errMessage += " present.";                                                                          \
        napi_throw_type_error(env, nullptr, errMessage.data());                                             \
        return returnVal;                                                                                   \
    }

#define JS_TIMER_BODY(method, type)                                                              \
    napi_value JsTimer::Set##method(napi_env env, napi_callback_info info_)                      \
    {                                                                                            \
        utils::NapiCallbackInfo info(env, info_);                                                \
        CHECK_ARGS_AND_THROW(method, info.GetArgc(), 1, nullptr);                                \
        JsTimer *timer = reinterpret_cast<JsTimer *>(info.GetData());                            \
        return utils::CreateUint32(env, timer->CreateTimerTask(env, info, TimerType::type));     \
    }                                                                                            \
                                                                                                 \
    napi_value JsTimer::SetWeak##method(napi_env env, napi_callback_info info_)                  \
    {                                                                                            \
        utils::NapiCallbackInfo info(env, info_);                                                \
        CHECK_ARGS_AND_THROW(Weak##method, info.GetArgc(), 1, nullptr);                          \
        JsTimer *timer = reinterpret_cast<JsTimer *>(info.GetData());                            \
        return utils::CreateUint32(env, timer->CreateWeakTimerTask(env, info, TimerType::type)); \
    }

JS_TIMER_BODY(Timeout, TIMEOUT)
JS_TIMER_BODY(Interval, INTERVAL)

#undef JS_TIMER_BODY

napi_value JsTimer::ClearTimer(napi_env env, napi_callback_info info_)
{
    utils::NapiCallbackInfo info(env, info_);
    CHECK_ARGS_AND_RETURN(info.GetArgc(), 1, nullptr);
    int64_t taskId = utils::GetValueInt64(env, info[0]);
    if (taskId <= 0)
    {
        return nullptr;
    }
    reinterpret_cast<JsTimer *>(info.GetData())->CancelTimer(taskId);
    return nullptr;
}

JsTimer::JsTimer(napi_env env) : env_(env)
{}

JsTimer::~JsTimer()
{}

TimerTask::~TimerTask()
{
    if (isCleanUpHook_)
    {
        napi_remove_env_cleanup_hook(env_, CleanUp, this);
    }
    napi_delete_reference(env_, callback_);
    for (auto &it : args_)
    {
        napi_delete_reference(env_, it);
    }
    args_.clear();
    env_ = nullptr;
    callback_ = nullptr;
}

TimerTask *TimerTask::Create(napi_env env, JsTimer *parent, TimerType type)
{
    return new TimerTask(env, parent, type);
}

void TimerTask::Delete()
{
    if (handle_.data != nullptr)
    {
        if (uv_is_closing(reinterpret_cast<uv_handle_t *>(&handle_)))
        {
            TIMER_LOG(DEBUG) << "handle is closing.";
            return;
        }
        uv_timer_stop(&handle_);
        uv_close(reinterpret_cast<uv_handle_t *>(&handle_), [](uv_handle_t *handle) {
            auto timer = reinterpret_cast<uv_timer_t *>(handle);
            ::operator delete(reinterpret_cast<TimerTask *>(timer->data));
        });
    }
    else
    {
        ::operator delete(this);
    }
}

int TimerTask::Init(uint32_t timeout)
{
    uv_loop_t *loop = nullptr;
    RUNNER_NAPI_CALL(env_, napi_get_uv_event_loop(env_, &loop), -1);
    RUNNER_UV_CALL(env_, uv_timer_init(loop, &handle_), -2)                                          // -2: return code
    RUNNER_UV_CALL(env_, uv_timer_start(&handle_, TimerTask::UvTimerCallback, timeout, timeout), -3) // -3: return code
    handle_.data = this;
    return 0;
}

void TimerTask::Execute() const
{
    utils::NapiHandleScope scope(env_);
    std::vector<napi_value> args;
    for (auto &arg : args_)
    {
        args.push_back(utils::GetReferenceValue(env_, arg));
    };
    napi_call_function(env_, utils::GetGlobal(env_), utils::GetReferenceValue(env_, callback_), args.size(),
                       args.data(), nullptr);
    // The timer may be freed after the user call; do not use it after this point.
}

void TimerTask::Unref()
{
    uv_unref(reinterpret_cast<uv_handle_t *>(&handle_));
}

void TimerTask::UvTimerCallback(uv_timer_t *handle)
{
    auto that = reinterpret_cast<TimerTask *>(handle->data);
    that->Execute();
    if (that->type_ == TimerType::TIMEOUT && that->parent_)
    {
        that->parent_->CancelTimer(that);
    }
}

JsTimer *JsTimer::BindToGlobal(napi_env env, napi_value global)
{
    JsTimer *timer = new JsTimer(env);
#define DESC_NAPI_METHOD(name, callback) \
    {name, nullptr, callback, nullptr, nullptr, nullptr, napi_default_jsproperty, timer}

// clang-format off
#define EXPORT_TIMER_METHODS(name)                    \
    DESC_NAPI_METHOD("set" #name, Set##name),         \
    DESC_NAPI_METHOD("setWeak" #name, SetWeak##name)  \
    
    std::vector<napi_property_descriptor> timerProp{
        EXPORT_TIMER_METHODS(Timeout),
        EXPORT_TIMER_METHODS(Interval),
        DESC_NAPI_METHOD("clearTimeout", ClearTimer),
        DESC_NAPI_METHOD("clearInterval", ClearTimer),
    };
    // clang-format on

#undef EXPORT_TIMER_METHODS
#undef DESC_NAPI_METHOD

    napi_define_properties(env, global, timerProp.size(), timerProp.data());
    return timer;
};
